using System;
using System.IO;

using MC68000;

namespace GenesisCore
{
	enum VdpWriteMode
	{
		Read, Write
	}

	enum VdpDestination
	{
		VRAM, CRAM, VSRAM
	}

	public partial class Genesis : IMemoryController
	{
		private MC68K			m_CPU;
		private byte[]			m_CartridgeRom;
		private byte[]			m_WorkRam;

		private byte[]			m_VdpRegisters;
		private byte[]			m_VdpColourRam;
		private byte[]			m_VdpVideoRam;
		private byte[]			m_VdpScrollRam;
		private int				m_VdpSR;
		private VdpWriteMode	m_VdpWriteMode;
		private VdpDestination	m_VdpDestination;
		private int				m_VdpAddress;
		private bool			m_VdpVramFillPending;

		private byte			m_IoVersion;
		private bool[]			m_IoDataBit7;
		private bool[]			m_IoDataTh;
		private byte[]			m_IoCtrl;

		private bool[]			m_PadRight, m_PadLeft, m_PadUp, m_PadDown, m_PadA, m_PadB, m_PadC, m_PadStart;

		public Genesis()
		{
			this.m_CPU = new MC68K(this);
			this.m_WorkRam = new byte[0x10000];

			this.m_VdpRegisters = new byte[25];
			this.m_VdpColourRam = new byte[128];
			this.m_VdpVideoRam = new byte[65536];
			this.m_VdpScrollRam = new byte[80];
			this.m_FrameBuffer = new uint[320 * 224];
			this.m_Palette = new uint[64 * 3];

			this.m_IoVersion = 0xA0;
			this.m_IoDataBit7 = new bool[3];
			this.m_IoDataTh = new bool[3];
			this.m_IoCtrl = new byte[3];
			this.m_PadUp = new bool[3];
			this.m_PadDown = new bool[3];
			this.m_PadLeft = new bool[3];
			this.m_PadRight = new bool[3];
			this.m_PadA = new bool[3];
			this.m_PadB = new bool[3];
			this.m_PadC = new bool[3];
			this.m_PadStart = new bool[3];
		}

		#region ROM Handling
		public void LoadRom(string fileName)
		{
			this.m_CartridgeRom = new byte[0x400000];
			RomLoader.Load(fileName, this.m_CartridgeRom);
			this.Reset();
		}

		public string RomName
		{
			get
			{
				return System.Text.ASCIIEncoding.ASCII.GetString(this.m_CartridgeRom, 0x150, 32).Trim();
			}
		}
		#endregion ROM Handling

		#region Gamepad Handling
		public void SetGamepad(int pad, bool up, bool down, bool left, bool right, bool a, bool b, bool c, bool start)
		{
			this.m_PadUp[pad] = up;
			this.m_PadDown[pad] = down;
			this.m_PadLeft[pad] = left;
			this.m_PadRight[pad] = right;
			this.m_PadA[pad] = a;
			this.m_PadB[pad] = b;
			this.m_PadC[pad] = c;
			this.m_PadStart[pad] = start;
		}

		private sbyte GetPadDataRegister(int pad)
		{
			if (this.m_IoDataTh[pad])
			{
				return (sbyte)((this.m_IoDataBit7[pad] ? 128 : 0) |
					64 |
					(this.m_PadC[pad] ? 0 : 32) |
					(this.m_PadB[pad] ? 0 : 16) |
					(this.m_PadRight[pad] ? 0 : 8) |
					(this.m_PadLeft[pad] ? 0 : 4) |
					(this.m_PadDown[pad] ? 0 : 2) |
					(this.m_PadUp[pad] ? 0 : 1));
			}
			else
			{
				return (sbyte)((this.m_IoDataBit7[pad] ? 128 : 0) |
					0 |
					(this.m_PadStart[pad] ? 0 : 32) |
					(this.m_PadA[pad] ? 0 : 16) |
					0 |
					0 |
					(this.m_PadDown[pad] ? 0 : 2) |
					(this.m_PadUp[pad] ? 0 : 1));
			}
		}
		#endregion Gamepad Handling

		public void Reset()
		{
			this.m_CPU.Reset();
		}

		#region IMemoryController Members

		public sbyte ReadB(int address)
		{
			if (address < 0x400000)
			{
				return (sbyte)this.m_CartridgeRom[address];
			}
			else if (address >= 0xA10000 && address < 0xA10020) // IO Area
			{
				switch (address)
				{
					case 0xA10000: return (sbyte)this.m_IoVersion; // Version Number
					case 0xA10001: return (sbyte)this.m_IoVersion;
					case 0xA10002: return GetPadDataRegister(0); // Data registers
					case 0xA10003: return GetPadDataRegister(0);
					case 0xA10004: return GetPadDataRegister(1);
					case 0xA10005: return GetPadDataRegister(1);
					case 0xA10006: return GetPadDataRegister(2);
					case 0xA10007: return GetPadDataRegister(2);
					case 0xA10008: return 0x00; // Control registers
					case 0xA10009: return 0x00;
					case 0xA1000A: return 0x00;
					case 0xA1000B: return 0x00;
					case 0xA1000C: return 0x00;
					case 0xA1000D: return 0x00;
					case 0xA1000E: return -1; // TxData A
					case 0xA1000F: return -1;
					case 0xA10010: return 0; // RxData A
					case 0xA10011: return 0;
					case 0xA10012: return 0; // S-Ctrl A
					case 0xA10013: return 0;
					case 0xA10014: return -1; // TxData B
					case 0xA10015: return -1;
					case 0xA10016: return 0; // RxData B
					case 0xA10017: return 0;
					case 0xA10018: return 0; // S-Ctrl B
					case 0xA10019: return 0;
					case 0xA1001A: return -1; // TxData C
					case 0xA1001B: return -1;
					case 0xA1001C: return 0; // RxData C
					case 0xA1001D: return 0;
					case 0xA1001E: return 0; // S-Ctrl C
					case 0xA1001F: return 0;
				}
				throw new ArgumentException();
			}
			else if (address == 0xA11100) // BUSREQ
			{
				return 0;
			}
			else if (address >= 0xC00000 && address < 0xC00002) // VDP Data
			{
				throw new NotImplementedException();
			}
			else if (address >= 0xC00004 && address < 0xC00008) // VDP Control
			{
				return (sbyte)(this.m_VdpSR >> ((~address & 1) << 3));
			}
			else if (address >= 0xFF0000 && address < 0xFFFFFF) // Work RAM
			{
				return (sbyte)this.m_WorkRam[address - 0xFF0000];
			}

			return 0;
		}

		public short ReadW(int address)
		{
			if (address < 0x400000)
			{
				return (short)((this.m_CartridgeRom[address] << 8) |
					this.m_CartridgeRom[address + 1]);
			}
			else if (address >= 0xA10000 && address < 0xA1001F) // IO Area
			{
				// Return 
				return (short)(((byte)ReadB(address) << 8) |
					(byte)ReadB(address + 1));
			}
			else if (address >= 0xC00000 && address < 0xC00002) // VDP Data
			{
				throw new NotImplementedException();
			}
			else if (address >= 0xC00004 && address < 0xC00006) // VDP Control
			{
				return (short)this.m_VdpSR;
			}
			else if (address >= 0xFF0000 && address < 0xFFFFFF) // Work RAM
			{
				return (short)((this.m_WorkRam[address - 0xFF0000] << 8) |
					this.m_WorkRam[address - 0xFEFFFF]);
			}

			return 0;
		}

		public int ReadL(int address)
		{
			if (address < 0x400000) // Cartridge ROM
			{
				return (this.m_CartridgeRom[address] << 24) |
					(this.m_CartridgeRom[address + 1] << 16) |
					(this.m_CartridgeRom[address + 2] << 8) |
					this.m_CartridgeRom[address + 3];
			}
			else if (address >= 0xA10000 && address < 0xA1001D) // IO Area
			{
				return ((byte)ReadB(address) << 24) |
					((byte)ReadB(address + 1) << 16) |
					((byte)ReadB(address + 2) << 8) |
					(byte)ReadB(address + 3);
			}
			else if (address >= 0xC00000 && address < 0xC00002) // VDP Data
			{
				throw new NotImplementedException();
			}
			else if (address >= 0xC00004 && address < 0xC00006) // VDP Control
			{
				throw new NotImplementedException();
			}
			else if (address >= 0xFF0000 && address < 0xFFFFFF) // Work RAM
			{
				return (this.m_WorkRam[address - 0xFF0000] << 24) |
					(this.m_WorkRam[address - 0xFF0000 + 1] << 16) |
					(this.m_WorkRam[address - 0xFF0000 + 2] << 8) |
					this.m_WorkRam[address - 0xFF0000 + 3];
			}

			return 0;
		}

		public void WriteB(int address, sbyte value)
		{
			if (address < 0x400000)
			{
				throw new ArgumentException();
			}
			else if (address >= 0xA10000 && address < 0xA10020) // IO Area
			{
				switch (address)
				{
					case 0xA10002: m_IoDataBit7[0] = (value & 128) > 0; m_IoDataTh[0] = (value & 64) > 0; break; // Data registers
					case 0xA10003: m_IoDataBit7[0] = (value & 128) > 0; m_IoDataTh[0] = (value & 64) > 0; break;
					case 0xA10004: m_IoDataBit7[1] = (value & 128) > 0; m_IoDataTh[1] = (value & 64) > 0; break;
					case 0xA10005: m_IoDataBit7[1] = (value & 128) > 0; m_IoDataTh[1] = (value & 64) > 0; break;
					case 0xA10006: m_IoDataBit7[2] = (value & 128) > 0; m_IoDataTh[2] = (value & 64) > 0; break;
					case 0xA10007: m_IoDataBit7[3] = (value & 128) > 0; m_IoDataTh[2] = (value & 64) > 0; break;
					case 0xA10008: this.m_IoCtrl[0] = (byte)value; break; // Control registers
					case 0xA10009: this.m_IoCtrl[0] = (byte)value; break;
					case 0xA1000A: this.m_IoCtrl[1] = (byte)value; break;
					case 0xA1000B: this.m_IoCtrl[1] = (byte)value; break;
					case 0xA1000C: this.m_IoCtrl[2] = (byte)value; break;
					case 0xA1000D: this.m_IoCtrl[2] = (byte)value; break;
				}
			}
			else if (address >= 0xC00000 && address < 0xC00002) // VDP Data
			{
				if (this.m_VdpWriteMode != VdpWriteMode.Write) // Can only write if we're in write mode
				{
					return;
				}

				switch (this.m_VdpDestination)
				{
					case VdpDestination.CRAM:
						// With CRAM, duplicate the byte and write it as a word
						short word = (short)((((byte)value) << 8) | (byte)value);
						this.WriteW(address, word);
						break;
				}
			}
			else if (address >= 0xC00004 && address < 0xC00006) // VDP Control
			{
			}
			else if (address >= 0xFF0000 && address < 0xFFFFFF) // Work RAM
			{
				this.m_WorkRam[address - 0xFF0000] = (byte)value;
			}
		}

		private void DmaFill_Cram(short data)
		{
			this.m_VdpColourRam[this.m_VdpAddress & 0x7F] = (byte)data;
			int length = (this.m_VdpRegisters[20] << 8) | this.m_VdpRegisters[19];
			for (int i = 0; i < length; i++)
			{
				this.m_VdpColourRam[(this.m_VdpAddress ^ 1) & 0x7F] = (byte)(data >> 8);
				this.m_VdpAddress += this.m_VdpRegisters[15];
			}
			this.m_VdpVramFillPending = false;
		}

		private void DmaFill_Vram(short data)
		{
			this.m_VdpVideoRam[this.m_VdpAddress & 0xFFFF] = (byte)data;
			int length = (this.m_VdpRegisters[20] << 8) | this.m_VdpRegisters[19];
			for (int i = 0; i < length; i++)
			{
				if (this.m_VdpAddress == 0xc000)
				{ }
				this.m_VdpVideoRam[(this.m_VdpAddress ^ 1) & 0xFFFF] = (byte)(data >> 8);
				this.m_VdpAddress += this.m_VdpRegisters[15];
			}
			this.m_VdpVramFillPending = false;
		}

		private void DmaFill_VSram(short data)
		{
			//this.m_VdpVideoRam[this.m_VdpAddress & 0xFFFF] = (byte)data;
			//int length = (this.m_VdpRegisters[20] << 8) | this.m_VdpRegisters[19];
			//for (int i = 0; i < length; i++)
			//{
			//    this.m_VdpVideoRam[(this.m_VdpAddress ^ 1) & 0xFFFF] = (byte)(data >> 8);
			//    this.m_VdpAddress += this.m_VdpRegisters[15];
			//}
			this.m_VdpVramFillPending = false;
		}

		private void M68KVramCopy(byte[] ram, int source)
		{
			int length = (this.m_VdpRegisters[20] << 8) | this.m_VdpRegisters[19];
			for (int i = 0; i < length; i++)
			{
				short data = ReadW(source);
				source += 2;
				ram[this.m_VdpAddress] = (byte)(data >> 8);
				ram[this.m_VdpAddress + 1] = (byte)data;
				this.m_VdpAddress += this.m_VdpRegisters[15];
			}
		}

		public void WriteW(int address, short value)
		{
			if (address < 0x400000)
			{
				throw new ArgumentException();
			}
			else if (address >= 0xA10000 && address < 0xA10020) // IO Area
			{
			}
			else if (address >= 0xC00000 && address < 0xC00004) // VDP Data
			{
				if (this.m_VdpWriteMode != VdpWriteMode.Write) // Can only write if we're in write mode
				{
					return;
				}

				if (this.m_VdpVramFillPending)
				{
					switch (this.m_VdpDestination)
					{
						case VdpDestination.VRAM:
							this.DmaFill_Vram(value);
							break;
						case VdpDestination.CRAM:
							this.DmaFill_Cram(value);
							break;
						case VdpDestination.VSRAM:
							this.DmaFill_VSram(value);
							break;
					}
					return;
				}

				switch (this.m_VdpDestination)
				{
					case VdpDestination.VRAM:
						if (this.m_VdpAddress == 0xc268)
						{ }
						this.m_VdpVideoRam[this.m_VdpAddress & 0xFFFF] = (byte)(value >> 8);
						this.m_VdpVideoRam[(this.m_VdpAddress + 1) & 0xFFFF] = (byte)value;
						this.m_VdpAddress += this.m_VdpRegisters[15];
						break;
					case VdpDestination.CRAM:
						this.m_VdpColourRam[this.m_VdpAddress & 0x7F] = (byte)(value >> 8);
						this.m_VdpColourRam[(this.m_VdpAddress + 1) & 0x7F] = (byte)value;
						this.m_VdpAddress += this.m_VdpRegisters[15];
						break;
					case VdpDestination.VSRAM:
						this.m_VdpAddress += this.m_VdpRegisters[15];
						break;
				}
			}
			else if (address >= 0xC00004 && address < 0xC00008) // VDP Control
			{
				if (((value >> 13) & 0x7) == 4)
				{
					int regNumber = (value >> 8) & 31;
					if (regNumber == 4)
					{ }
					this.m_VdpRegisters[regNumber] = (byte)value;
				}
			}
			else if (address >= 0xFF0000 && address < 0xFFFFFF) // Work RAM
			{
				this.m_WorkRam[address - 0xFF0000] = (byte)(value >> 8);
				this.m_WorkRam[address - 0xFF0000 + 1] = (byte)value;
			}
		}

		public void WriteL(int address, int value)
		{
			if (address < 0x400000)
			{
				throw new ArgumentException();
			}
			else if (address >= 0xA10000 && address < 0xA10020) // IO Area
			{
			}
			else if (address >= 0xC00000 && address < 0xC00002) // VDP Data
			{
				// A 32-bit write works the same as two 16-bit writes
				WriteW(address, (short)(value >> 16));
				WriteW(address, (short)value);
			}
			else if (address >= 0xC00004 && address < 0xC00006) // VDP Control
			{
				if (((value >> 30) & 0x3) == 2) // Register set
				{
					WriteW(address, (short)(value >> 16));
					WriteW(address, (short)value);
				}
				else // Address Set & DMA
				{
					int c = ((value >> 30) & 3) | ((value >> 2) & 60);
					int a = ((value >> 16) & 16383) | ((value << 14) & 49152);

					this.m_VdpAddress = a;
					switch (c & 0xF)
					{
						case 0: this.m_VdpDestination = VdpDestination.VRAM; this.m_VdpWriteMode = VdpWriteMode.Read; break;
						case 1: this.m_VdpDestination = VdpDestination.VRAM; this.m_VdpWriteMode = VdpWriteMode.Write; break;
						case 3: this.m_VdpDestination = VdpDestination.CRAM; this.m_VdpWriteMode = VdpWriteMode.Write; break;
						case 4: this.m_VdpDestination = VdpDestination.VSRAM; this.m_VdpWriteMode = VdpWriteMode.Read; break;
						case 5: this.m_VdpDestination = VdpDestination.VSRAM; this.m_VdpWriteMode = VdpWriteMode.Write; break;
						case 8: this.m_VdpDestination = VdpDestination.CRAM; this.m_VdpWriteMode = VdpWriteMode.Read; break;
					}

					// Check for DMA operation
					if ((c & 32) == 32)
					{
						if ((this.m_VdpRegisters[23] & 128) == 0) // 68K -> VDP
						{
							int source = (this.m_VdpRegisters[21] << 1) |
								(this.m_VdpRegisters[22] << 9) |
								(this.m_VdpRegisters[23] << 17);

							switch (this.m_VdpDestination)
							{
								case VdpDestination.CRAM: this.M68KVramCopy(this.m_VdpColourRam, source); break;
								case VdpDestination.VRAM: this.M68KVramCopy(this.m_VdpVideoRam, source); break;
								case VdpDestination.VSRAM: this.M68KVramCopy(this.m_VdpScrollRam, source); break;
							}
						}
						else if ((this.m_VdpRegisters[23] & 64) == 0) // VRAM Fill
						{
							this.m_VdpVramFillPending = true;
						}
						else if ((this.m_VdpRegisters[23] & 64) == 64) // VRAM Copy
						{
							throw new NotImplementedException();
						}
					}
				}
			}
			else if (address >= 0xFF0000 && address < 0xFFFFFF) // Work RAM
			{
				this.m_WorkRam[address - 0xFF0000] = (byte)(value >> 24);
				this.m_WorkRam[address - 0xFF0000 + 1] = (byte)(value >> 16);
				this.m_WorkRam[address - 0xFF0000 + 2] = (byte)(value >> 8);
				this.m_WorkRam[address - 0xFF0000 + 3] = (byte)value;
			}
		}

		#endregion
	}
}
